Skip to content

fix(tour): tour replays after skip due to poisoned history stack#19

Merged
maggo83 merged 3 commits intok9ert:mainfrom
maggo83:fix/tour-replays-after-skip
Mar 9, 2026
Merged

fix(tour): tour replays after skip due to poisoned history stack#19
maggo83 merged 3 commits intok9ert:mainfrom
maggo83:fix/tour-replays-after-skip

Conversation

@maggo83
Copy link
Copy Markdown
Collaborator

@maggo83 maggo83 commented Mar 6, 2026

Problem

After triggering "Restart Tour" from Preferences and then skipping the tour, the tour would unexpectedly restart when navigating back to the main menu from WalletMenu.

Root Cause

show_menu("start_intro_tour") sets current_menu_id to "start_intro_tour" and never resets it after launching the tour overlay. Any subsequent forward navigation (e.g. into WalletMenu) calls push_menu(), which pushes "start_intro_tour" onto the history stack. On the way back, pop_menu() restores "start_intro_tour" as the current menu, hitting the end-of-show_menu check that unconditionally re-launches the tour — regardless of whether the user had already skipped it.

Fix

Reset current_menu_id to "main" immediately before launching the tour overlay. This prevents the sentinel value from ever reaching the history stack.

Repro Steps

  1. Add a wallet (Add Wallet → Generate Seedphrase → Create)
  2. Go to Settings → User Preferences → Restart Tour
  3. Skip the tour
  4. Click the wallet bar to go to Wallet menu
  5. Edit the wallet name and confirm with the checkmark
  6. Hit Back

Expected: Returns to main menu
Actual (before fix): Tour restarts

@al-munazzim
Copy link
Copy Markdown
Contributor

Review: PR #19 — Tour replay fix + UI overhaul

This PR is titled as a tour bug fix but is actually a massive UI refactor (8,664 additions / 5,007 deletions across 100+ files, 31 commits). The tour fix itself is a one-liner. The rest is a substantial rework of the MockUI architecture.

👍 What works well

  • The actual bug fix is clean and correct. current_menu_id was leaking "start_intro_tour" into the history stack. Resetting to "main" before launching the tour overlay is the right fix — prevents the sentinel value from poisoning push_menu()/pop_menu().

  • TitledScreen base class — good refactor. Extracts the repeated title bar + back button + body pattern from GenericMenu, ActionScreen, etc. into a shared base. Reduces ~200 lines of duplicated layout code.

  • Template method pattern on GenericMenuTITLE_KEY class attribute + get_menu_items() + post_init() hooks. Clean way to convert factory functions to proper subclasses without breaking the call sites.

  • Icon generation overhaul — switching from bytes([...]) to bytes literals is a real fix: 126 icons × 1764 bytes = 217KB on a 38KB heap is a genuine OOM. One .py per icon also fixes the mpy-cross OOM on the monolith. Well-documented in the commit message.

  • Test coverage — 13 device tests, scenario-based test structure, fresh firmware flash per module. The regression test for the tour replay bug is correctly placed in the "restart from existing navigation context" scenario rather than the cold-boot path.

  • Settings menu restructure — splitting 10+ items into focused submenus (Security, Storage, Preferences) makes sense for the 800×480 touch target. The dual-entry for Backups (in both Security and Storage) is a pragmatic UX choice.

⚠️ Issues

1. PR scope — this should be multiple PRs
The title says "fix(tour): tour replays after skip" but this includes:

  • Tour replay bug fix (1 commit, ~5 lines)
  • TitledScreen refactor (architectural change)
  • UI constant scaling for 800×480 (visual change)
  • Icon generation overhaul + 100+ new icon files
  • Settings menu restructure
  • GenericMenu template method refactor
  • Simulator build system fixes
  • Dead code removal (menu_id, *args/**kwargs)
  • i18n additions
  • Test infrastructure overhaul

Each of these is a reviewable, revertable unit. Bundling them makes it nearly impossible to git bisect if something breaks on hardware, and reviewing 8K+ lines for correctness is impractical. Consider splitting in future — at minimum separating the icon files, the refactor, and the bug fix.

2. conftest.py widget tree navigation is brittle

# root[0] = device_bar, [2] = right_container, [1] = settings_btn

Hardcoded child indices into the LVGL widget tree. Any layout change (adding a widget, reordering containers) silently breaks these tests. The _find_settings_btn_index() helper mitigates this somewhat, but the comments acknowledge the fragility. Consider adding lv.obj.FLAG markers or widget names for test-critical elements.

3. _extract_intro_tour_steps parses source code with ast
The test file parses navigation_controller.py source to extract INTRO_TOUR_STEPS at import time. This works but is unusual and breaks if the constant is ever moved to a config file or computed dynamically. A simpler approach: export the step keys as a module-level constant that tests can import directly.

4. Language selector removed from device bar without migration path
The lang_lbl click handler is removed from DeviceBar and language selection is now only accessible via Settings → Language. This is a UX regression for users who relied on the always-visible language indicator — especially important for a device that ships to non-English speakers who might struggle navigating menus in English to find the language setting. The new "Select Language (DE)" menu item with inline lang code helps, but it's buried deeper in the menu hierarchy.

📝 Minor

  • The merge commits (3 of them) add noise — consider rebasing before merge for a cleaner history.
  • Commit 6a10f3c has an SHA ending in 666666 — the git odds gods smiled on this one.
  • Several commit messages are truncated in the subject line (GitHub's 72-char limit) — the bodies are well-written though.

Verdict

The tour bug fix is correct and well-understood. The refactoring work (TitledScreen, template method pattern, icon generation) is solid engineering. The main concern is PR size — this is 31 commits across architectural, visual, build system, and test changes. It works as a single merge if you trust it end-to-end, but it's un-reviewable in the traditional sense. Future PRs should be smaller and focused.

maggo83 added 2 commits March 7, 2026 11:28
When the user triggerred "Restart Tour" from Preferences,
show_menu("start_intro_tour") set current_menu_id to "start_intro_tour"
and never reset it after the tour overlay was started.  Any subsequent
forward navigation (e.g. into WalletMenu) called push_menu(), which
pushed "start_intro_tour" onto the history stack.  On the way back,
pop_menu() restored "start_intro_tour" as the current menu, hitting
the end-of-show_menu check that unconditionally re-launches the tour —
regardless of whether the user had already skipped it.

Fix: immediately before launching the tour overlay, reset
current_menu_id to "main".  This prevents the sentinel value from ever
reaching the history stack, so navigating back from any downstream menu
returns to the main menu without re-triggering the tour.
Replace 10 individual test_a..test_j functions with two scenario-based
tests that each encode a complete sequential user journey:

- test_tour_skip_scenario  : initial boot tour (overlay mechanics + skip path)
- test_tour_complete_scenario : manual restart + regression check + full completion

Key changes:
- Module fixture now flashes firmware fresh, giving a truly clean state
  (no ui_state_config.json) so the tour auto-start is the real first-boot
  experience rather than a manually injected state
- Regression test for 'tour retriggers after skip + navigate-back' moved
  to scenario 2 where it is meaningful: the bug only manifests when the
  tour is launched from within an existing navigation context (back-stack
  present), not on initial boot with an empty history
- Extract flash_firmware() helper in conftest so both _require_device and
  the tour module fixture share the same flash+wait logic
- State machine is now explicit and self-documenting via section comments
@maggo83 maggo83 force-pushed the fix/tour-replays-after-skip branch from 38f3f27 to 3d11e3c Compare March 7, 2026 10:28
@maggo83
Copy link
Copy Markdown
Collaborator Author

maggo83 commented Mar 7, 2026

  1. The "big "changes found were all already included in PR feat(MockUI): resize all GUI elements for 800×480 touch display #18. Rebased PR fix(tour): tour replays after skip due to poisoned history stack #19 so now only two commits remain: 1 one line (actual fix) and 1 update for the tests.

  2. The cleaner solution would be widget name, but this should be rolled out on general and is bigger rework fit for its own PR -> noted on to do list.

  3. That choice was made deliberately, because your proposal (which we initially tried) would still not work, because navigation controller imports lvgl directly which would not work on the host computer outside the simulator. The next level workaround could be to put only these steps into a separate file, but for now we wanted to avoid this overhead.

  4. This was a user feedback to reduce the information on the "main" menu. The GEAR symbol is assumed universally understood as settings and language (always with the language code) is explicitly put at the top of the menu to be easily findable in case you switched to an unknown language. See also comments from the PR feat(MockUI): resize all GUI elements for 800×480 touch display #18 and its commits where this change was introduced.

@maggo83
Copy link
Copy Markdown
Collaborator Author

maggo83 commented Mar 8, 2026

@k9ert : Do you think my last comment addresses the findings sufficiently or do you think 2./3. need to be reworked now prior merge?

Copy link
Copy Markdown
Contributor

@al-munazzim al-munazzim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

CI: ✅ Build Firmware passes

The Fix (navigation_controller.py)

Clean and correct. The one-liner self.ui_state.current_menu_id = "main" right before GuidedTour(...).start() is the minimal fix — prevents the sentinel "start_intro_tour" from ever polluting the history stack. The comment block explaining why is excellent.

No concerns on the fix itself. 👍

Test Refactor (test_tour_device.py)

The consolidation from 10 individual test_a..test_j into 2 scenario functions is a good call:

Positives:

  • Module fixture now flashes firmware for a truly clean first-boot state — much more realistic than manually writing tour_completed=False
  • Regression test correctly placed in scenario 2 where the bug actually manifests (needs an existing back-stack)
  • Section comments make the state machine explicit and easy to follow
  • flash_firmware() extraction in conftest avoids duplication

One nit:
The two tests are explicitly order-dependent (scenario 2 relies on state from scenario 1). The docstring is honest about this, but pytest doesn't guarantee order by default — it only happens to work because of alphabetical sorting. If someone adds pytest-randomly or reorders, things break silently. Consider either combining into a single test function, or adding a precondition guard at the top of scenario 2 (assert _tour_completed()).

conftest.py

flash_firmware() helper extraction is clean. The else branch fix in _require_device is also an improvement — previously it always waited even after flashing (double wait).

Verdict: Approve. Minimal, correct fix with well-improved tests.

@maggo83 maggo83 merged commit 9b12a04 into k9ert:main Mar 9, 2026
1 check passed
@maggo83 maggo83 deleted the fix/tour-replays-after-skip branch March 9, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants